package Managers;

import Entities.Category;
import Entities.Medium;
import Tools.DBTools;
import Tools.DatabaseException;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.*;
import org.xmldb.api.modules.XMLResource;
import org.xmldb.api.modules.XPathQueryService;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Oliver Mrazik on 5. 6. 2014.
 * author: Oliver Mrazik
 * version: 2014-06-05
 */
public class LibraryManagerImpl implements LibraryManager {

    public static final Logger logger = LoggerFactory.getLogger(LibraryManagerImpl.class);

    private String mediumCollection;
    private String categoryCollection;
    private String login;
    private String pass;

    /**
     * Constructor which initialize database and collection
     * @param mediumCollection collection of mediums to initialize.
     * @param categoryCollection collection of categories to initialize
     * @param name login to database
     * @param password password for database
     * @throws Tools.DatabaseException when error occurs while setting up driver
     */
    public LibraryManagerImpl(String mediumCollection, String categoryCollection, String name, String password) throws DatabaseException {
        try {
            String driver = "org.exist.xmldb.DatabaseImpl";
            Class cl = Class.forName(driver);
            Database database = (Database) cl.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);

            this.mediumCollection = mediumCollection;
            this.categoryCollection = categoryCollection;
            this.login = name;
            this.pass = password;

        } catch (Exception ex) {
            logger.warn("Error initializing database connection.", ex);
            throw new DatabaseException("Error initializing database connection.", ex);
        }

    }

    /**
     * Method for access to database and return selected collection.
     *
     * @param collection desired collection
     * @return selected collection
     * @throws Tools.DatabaseException when error occurs when getting collection.
     */
    private Collection getColl(String collection) throws DatabaseException {
        try {
            String URI = "xmldb:exist://localhost:8899/exist/xmlrpc";
            //String URI = "xmldb:exist://localhost:8080/exist/xmlrpc";
            return DatabaseManager.getCollection(URI + collection, login, pass);
        } catch (XMLDBException ex) {
            logger.debug("Get collection error");
            throw new DatabaseException(ex);
        }
    }

    @Override
    public void insertMediumIntoCategory(Medium medium, Category category) throws DatabaseException {

        //============================================================================================================//
        //get database collections
        Collection catCol = this.getColl(categoryCollection);
        if (catCol == null) {
            logger.warn("Category collection is null");
            throw new DatabaseException("Category collection is null");
        }
        //============================================================================================================//

        //============================================================================================================//
        //add medium to category & test if it is not already inside
        XMLResource resource = null;
        Document doc;
        try {
            String xPath = String.format("//category[@name='%s']//medium[text()='%s']", category.getName(), medium.getId());

            XPathQueryService service = (XPathQueryService) catCol.getService("XPathQueryService", "1.0");
            ResourceSet resourceSet = service.query(xPath);
            if (resourceSet.getSize() != 0) {
                logger.info("Medium already exist in category.");
                throw new DatabaseException("Medium already exist in category.");
            }

            resource = (XMLResource) catCol.getResource(category.getId());
            if (resource == null) {
                // this should never happen!
                logger.debug("Category with name \"" + category.getName() + "\" is not in database.");
            } else {
                doc = (Document) resource.getContentAsDOM();
                Element root = (Element) doc.getFirstChild();
                Element mediumID = doc.createElement("medium");
                mediumID.setTextContent(medium.getId());
                root.getElementsByTagName("mediums").item(0).appendChild(mediumID);

                resource.setContentAsDOM(doc);
                catCol.storeResource(resource);

                List<String> temp = new ArrayList<>();

                //if category is empty so far
                if (category.getMediums() != null) {
                    temp.addAll(category.getMediums());
                }

                //now add newly added medium
                temp.add(medium.getId());
                category.setMediums(temp);
                logger.info("Medium \"" + medium.getLabel() + "\" successfully inserted into category \"" + category.getName() + "\"");
            }
        } catch (XMLDBException ex) {
            logger.debug("Error adding medium to category.", ex);
            throw new DatabaseException("Error adding medium to category.", ex);
        } finally {
            DBTools.resourceClose(resource);
            DBTools.collectionClose(catCol);
        }
        //============================================================================================================//

    }

    @Override
    public void removeMediumFromCategory(Medium medium, Category category) throws DatabaseException {

        //============================================================================================================//
        //get database collections
        Collection catCol = this.getColl(categoryCollection);
        if (catCol == null) {
            logger.warn("Category collection is null");
            throw new DatabaseException("Category collection is null");
        }
        //============================================================================================================//

        //============================================================================================================//
        //remove medium from category & test if it is inside
        try {
            String xPath = String.format("//category[@name='%s']//medium[text()='%s']", category.getName(), medium.getId());

            XPathQueryService service = (XPathQueryService) catCol.getService("XPathQueryService", "1.0");
            ResourceSet resourceSet = service.query(xPath);
            if (resourceSet.getSize() <= 0) {
                logger.info("No such medium in this category.");
                throw new DatabaseException("No such medium in this category.");
            }

            String xQuery = String.format("for $med in collection('/%s')//category[@name='%s']//medium\n" +
                    "where $med[text()='%s']\n" +
                    "\treturn update delete $med", categoryCollection, category.getName(), medium.getId());

            service.query(xQuery);

            List<String> temp = category.getMediums();
            temp.remove(medium.getId());
            category.setMediums(temp);

            logger.info("Medium \"" + medium.getLabel() + "\" successfully removed from category \"" + category.getName() + "\"");
        } catch (XMLDBException ex) {
            logger.debug("Error removing medium from category", ex);
            throw new DatabaseException("Error removing medium from category", ex);
        } finally {
            DBTools.collectionClose(catCol);
        }
    }

    @Override
    public Category getCategoryByMedium(Medium medium) throws DatabaseException {
        //============================================================================================================//
        //get database collections
        Collection catCol = this.getColl(categoryCollection);
        if (catCol == null) {
            logger.warn("Category collection is null");
            throw new DatabaseException("Category collection is null");
        }
        //============================================================================================================//

        //============================================================================================================//
        //find and return category
        XMLResource resource = null;
        Category ret = new Category();
        try {
            String xPath = String.format("//category[.//medium[text()='%s']]", medium.getId());
            String xQuery = String.format("for $doc in collection('/%s')\n" +
                    "where $doc//category[.//medium[text()='%s']]\n" +
                    "\treturn util:document-name($doc)", categoryCollection, medium.getId());

            XPathQueryService service = (XPathQueryService) catCol.getService("XPathQueryService", "1.0");
            //get id
            ResourceSet resourceSet = service.query(xQuery);

            resource = (XMLResource) resourceSet.getResource(0);
            String id = (String) resource.getContent();
            //id = id.substring(0, id.length() - 4);

            //get category
            resourceSet = service.query(xPath);

            if (resourceSet.getSize() != 1) {
                logger.error("[THIS SHOULD NEVER HAPPEN] : Medium is in more than one category.");
                throw new DatabaseException("[THIS SHOULD NEVER HAPPEN] : Medium is in more than one category.");
            }

            resource = (XMLResource) resourceSet.getResource(0);
            Document doc = (Document) resource.getContentAsDOM();

            //set id
            ret.setId(id);

            //set name
            ret.setName(doc.getFirstChild().getAttributes().getNamedItem("name").getNodeValue());

            //set mediums
            NodeList mediums = doc.getElementsByTagName("mediums");
            List<String> mediumList = new ArrayList<>();

            for (int i = 0; i < mediums.getLength(); i++) {
                mediumList.add(mediums.item(i).getTextContent());
            }

            ret.setMediums(mediumList);

        } catch (XMLDBException ex) {
            logger.debug("Error searching category by medium.", ex);
            throw new DatabaseException("Error searching category by medium.", ex);
        } finally {
            DBTools.collectionClose(catCol);
            DBTools.resourceClose(resource);
        }
        return ret;
    }

    @Override
    public Document getMediumAsXMLDocument(String id) throws DatabaseException {

        if (id == null) {
            logger.debug("id is null");
            throw new NullPointerException("Id is null.");
        }

        if (id.isEmpty() || id.equals("")) {
            logger.debug("id is empty");
            throw new IllegalArgumentException("Id is empty.");
        }

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl(mediumCollection);
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //create new document
        XMLResource resource = null;
        Document doc = null;
        try {
            resource = (XMLResource) col.getResource(id);
            if (resource == null) {
                logger.info("Medium with id \"" + id + "\" is not in database.");
                return null;
            }
            doc = (Document) resource.getContentAsDOM();
        } catch (XMLDBException ex) {
            logger.debug("Error creating XMLDocument from medium entity", ex);
            throw new DatabaseException("Error creating XMLDocument from medium entity", ex);
        } finally {
            DBTools.collectionClose(col);
            DBTools.resourceClose(resource);
        }
        return doc;
    }

    @Override
    public String getMediumAsString(String id) throws DatabaseException {
        if (id == null) {
            logger.debug("id is null");
            throw new NullPointerException("Id is null.");
        }

        if (id.isEmpty() || id.equals("")) {
            logger.debug("id is empty");
            throw new IllegalArgumentException("Id is empty.");
        }

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl(mediumCollection);
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //create new document
        XMLResource resource = null;
        Document doc;
        String mediumXML = "";
        try {
            resource = (XMLResource) col.getResource(id);
            if (resource == null) {
                logger.info("Medium with id \"" + id + "\" is not in database.");
                return null;
            }
            doc = (Document) resource.getContentAsDOM();

            DOMSource dom = new DOMSource(doc);
            mediumXML = transformToString(dom, "med");

        } catch (XMLDBException ex) {
            logger.debug("Error creating XMLDocument from medium entity", ex);
            throw new DatabaseException("Error creating XMLDocument from medium entity", ex);
        } finally {
            DBTools.collectionClose(col);
            DBTools.resourceClose(resource);
        }
        return mediumXML;
    }

    @Override
    public String getAllMediumsAsXMLDocument() throws DatabaseException {

        //------------------------------------------------------------------------------------------------------------//
        //get collection with mediums
        Collection col = this.getColl(mediumCollection);
        if (col == null) {
            logger.warn("Collection is null.");
            throw new DatabaseException("Collection is null.");
        }
        //------------------------------------------------------------------------------------------------------------//

        //------------------------------------------------------------------------------------------------------------//
        //gathering mediums
        /**
         * TESTING PURPOSE ONLY=========================================================================================
         */
//        XMLResource resource = null;
        /**
         * =============================================================================================================
         */
        Document list;
        String mediumXML = "";
        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            list = docBuilder.newDocument();
            Element root = list.createElement("mediums");
            list.appendChild(root);

            String[] resources = col.listResources();
            for (int i = 0; i < col.getResourceCount(); i++) {
                //resources[i] = resources[i].substring(0, resources[i].length() - 4);
                Document tempDoc = getMediumAsXMLDocument(resources[i]);

                Node medium = tempDoc.getFirstChild().cloneNode(true);
                Node newNode = list.importNode(medium, true);
                root.appendChild(newNode);
            }

            DOMSource dom = new DOMSource(list);
            mediumXML = transformToString(dom, "medAll");

            /**
             * TESTING PURPOSE ONLY=====================================================================================
             */
//            resource = (XMLResource) col.createResource("AllMediumsTest", "XMLResource");
//            resource.setContentAsDOM(list);
//            col.storeResource(resource);
            /**
             * =========================================================================================================
             */
        } catch(XMLDBException | ParserConfigurationException ex) {
            logger.debug("Error listing all mediums", ex);
            throw new DatabaseException("Error listing all mediums", ex);
        } finally {
            DBTools.collectionClose(col);
            /**
             * TESTING PURPOSE ONLY=====================================================================================
             */
//            DBTools.resourceClose(resource);
            /**
             * =========================================================================================================
             */
        }

        //------------------------------------------------------------------------------------------------------------//
        return mediumXML;
    }

    @Override
    public Document getCategoryWithMediumsAsXMLDocument(String id) throws DatabaseException {
        //============================================================================================================//
        //get database collections
        Collection medCol = this.getColl(mediumCollection);
        Collection catCol = this.getColl(categoryCollection);
        if (catCol == null | medCol == null) {
            logger.warn("One or both collections are null");
            throw new DatabaseException("One or both collections are null");
        }
        //============================================================================================================//

        //============================================================================================================//
        //create new document with category and it's mediums
        /**
         * TESTING PURPOSE ONLY=========================================================================================
         */
//        XMLResource resource = null;
        /**
         * =============================================================================================================
         */
        Document doc = null;
        XMLResource catResource = null;
        XMLResource medResource = null;
        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            doc = docBuilder.newDocument();
            //create root node
            Element root = doc.createElement("category");
            doc.appendChild(root);

            //create mediums node
            Element mediums = doc.createElement("mediums");
            root.appendChild(mediums);

            //get all mediums id in category
            catResource = (XMLResource) catCol.getResource(id);
            Document catDoc = (Document) catResource.getContentAsDOM();
            NodeList mediumIdList = catDoc.getElementsByTagName("medium");

            //get attribute "name"
            root.setAttribute("name", catDoc.getFirstChild().getAttributes().getNamedItem("name").getTextContent());

            //build our $values parameter for xQuery
            StringBuilder strBuilder = new StringBuilder("(");
            for (int i = 0; i < mediumIdList.getLength(); i++) {
                String medID = mediumIdList.item(i).getTextContent();
                if (i != 0) {
                    strBuilder.append(", ");
                }
                strBuilder.append("'");
                strBuilder.append(medID);
                //strBuilder.append(".xml");
                strBuilder.append("'");
            }
            strBuilder.append(")");
            //System.out.println(strBuilder.toString());

            //get all mediums documents from database
            String xQuery = String.format("let $values := %s\n" +
                    "for $doc in collection('/%s')\n" +
                    "where util:document-name($doc) = $values\n" +
                    "return $doc ", strBuilder.toString(), mediumCollection);
            XPathQueryService service = (XPathQueryService) medCol.getService("XPathQueryService", "1.0");
            ResourceSet resourceSet = service.query(xQuery);
            //System.out.println(resourceSet.getSize());

            //create from every resource document and write it into our result document
            ResourceIterator iterator = resourceSet.getIterator();

            while (iterator.hasMoreResources()) {
                medResource = (XMLResource) iterator.nextResource();
                Document medium = (Document) medResource.getContentAsDOM();

                Node wholeDocument = medium.getFirstChild().cloneNode(true);
                Node wholeMediumDocument = doc.importNode(wholeDocument, true);
                mediums.appendChild(wholeMediumDocument);
            }

            /**
             * TESTING PURPOSE ONLY=====================================================================================
             */
//            resource = (XMLResource) catCol.createResource("CategoryWithMediums", "XMLResource");
//            resource.setContentAsDOM(doc);
//            catCol.storeResource(resource);
            /**
             * =========================================================================================================
             */

        } catch (XMLDBException | ParserConfigurationException ex) {
            logger.debug("Error creating new document.", ex);
            throw new DatabaseException("Error creating new document.", ex);
        } finally {
            DBTools.resourceClose(catResource);
            DBTools.resourceClose(medResource);
            DBTools.collectionClose(catCol);
            DBTools.collectionClose(medCol);
            /**
             * TESTING PURPOSE ONLY=========================================================================================
             */
//                DBTools.resourceClose(resource);
            /**
             * =============================================================================================================
             */
        }
        return doc;
    }

    @Override
    public String getDatabaseContentAsXMLDocument(Boolean isAll) throws DatabaseException {
        //============================================================================================================//
        //get database collections
        Collection catCol = this.getColl(categoryCollection);
        if (catCol == null) {
            logger.warn("Categories collection is null");
            throw new DatabaseException("Categories collection is null");
        }
        //============================================================================================================//

        //============================================================================================================//
        //gathering database
        /**
         * TESTING PURPOSE ONLY=========================================================================================
         */
//        XMLResource resource = null;
        /**
         * =============================================================================================================
         */
        Document library;
        String libraryXML = "";
        try {
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

            library = docBuilder.newDocument();
            Element root = library.createElement("videolibrary");
            library.appendChild(root);

            String[] resources = catCol.listResources();
            for (String id : resources) {
                Document tempDoc = getCategoryWithMediumsAsXMLDocument(id);

                Node category = tempDoc.getFirstChild().cloneNode(true);
                Node newNode = library.importNode(category, true);
                root.appendChild(newNode);
            }

            DOMSource dom = new DOMSource(library);
            if (isAll) {
                libraryXML = transformToString(dom, "all");
            } else {
                libraryXML = transformToString(dom, "cat");
            }

            /**
             * TESTING PURPOSE ONLY=====================================================================================
             */
//           resource = (XMLResource) catCol.createResource("AllMediumsTest", "XMLResource");
//            resource.setContentAsDOM(library);
//            catCol.storeResource(resource);
            /**
             * =========================================================================================================
             */

        } catch (ParserConfigurationException | XMLDBException ex) {
            logger.debug("Error listing whole database.", ex);
            throw new DatabaseException("Error listing whole database.", ex);
        } finally {
            DBTools.collectionClose(catCol);
            /**
             * TESTING PURPOSE ONLY=====================================================================================
             */
//            DBTools.resourceClose(resource);
            /**
             * =========================================================================================================
             */
        }


        return libraryXML;
    }

    private String transformToString(DOMSource domSource, String what) {

        InputStream is = null;
        try {
            switch (what) {
                case "all" : is = getClass().getClassLoader().getResourceAsStream("DatabaseContent.xsl");
                             break;
                case "cat" : is = getClass().getClassLoader().getResourceAsStream("CategoryList.xsl");
                             break;
                case "medAll" : is = getClass().getClassLoader().getResourceAsStream("MediumList.xsl");
                             break;
                case "med" : is = getClass().getClassLoader().getResourceAsStream("Medium.xsl");
                             break;
                    default: is = null;
            }
//            if (what.equals("all")) {
//                is = getClass().getClassLoader().getResourceAsStream("DatabaseContent.xsl");
//            } else if (what.equals("med")) {
//                is = getClass().getClassLoader().getResourceAsStream("MediumList.xsl");
//            } else if (what.equals("cat")) {
//                is = getClass().getClassLoader().getResourceAsStream("CategoryList.xsl");
//            }

            //final StreamSource styleSource = new StreamSource(is);
            Source xslt = new StreamSource(is);
            TransformerFactory tf = TransformerFactory.newInstance();
            //Templates templates = tf.newTemplates(styleSource);
            Transformer transformer = tf.newTransformer(xslt);
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

            StringWriter sw = new StringWriter();
            StreamResult sr = new StreamResult(sw);

            transformer.transform(domSource, sr);

            return sw.toString();
        } catch (TransformerException ex) {
            logger.debug("Error while transforming to string.");
            return null;
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException ex) {
                logger.debug("Error closing input stream.");
            }
        }
    }

}

